package pkg.genericity.utils;


import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.log4j.Logger;

import pkg.genericity.DO.Criteria;
import pkg.genericity.annot.Parser;

/**
 * 
 * @author Farrukh Shakil
 *
 */
public class BeanUtil {
	
	private Class<?> bean;
	private String []fields;
	private FieldInfo []fieldsInfo;
	private Logger log = Logger.getLogger(Criteria.class.getName());
	public BeanUtil()
	{
		
	}
	
	public BeanUtil(Class<?> bean) {
		this.bean=bean;
	}
	
	/**
	 * Sets the field Bean of BeanUtil class. Basically used via JSTL
	 * @param className
	 */
	public void setBean(String className) {
		try {
			this.bean= Class.forName(className);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Overloaded: Returns a Bean of given classname populated by given parameter map
	 * @param className
	 * @param params
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Object getPopulatedBean(String className, Map<?, ?> params) throws IllegalArgumentException {
		try {
			return getPopulatedBean(Class.forName(className), params);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	/**
	 * Overloaded: Returns a Bean of already set classname (by setBean method) populated by given parameter map. To be used via JSTL
	 * @param params
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Object getpopulatedBean(Map<?, ?> params) throws IllegalArgumentException
	{
			return (this.bean != null) ?getPopulatedBean(bean,params):null;
	}
	
	/**
	 * Overloaded: Returns a Bean of given class populated by given parameter map
	 * @param beanclass
	 * @param params
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Object getPopulatedBean(Class<?> beanclass, Map<?, ?> params) throws IllegalArgumentException {
		try {
			Object beaninstance = beanclass.newInstance();
			Parser parser = new Parser();
			for (Object key : params.keySet()) {
				Field field =beanclass.getDeclaredField(key.toString());
				
				if (field != null) {
					if (parser.isRequired(field) && params.get(key) == null ) {
						throw new IllegalArgumentException(field.getName()+" is a required field and its empty");
					}
					beaninstance = updateField(beaninstance, key.toString(), params.get(key));
				}
			}
			return beaninstance;
			
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} 
		return null;
	}
	
	/**
	 * Updates a field of an object by given value
	 * @param beaninstance
	 * @param fieldname
	 * @param value
	 * @return
	 */
	public Object updateField(Object beaninstance, String fieldname, Object value)
	{
		try {
			Class<?> beanclass= beaninstance.getClass();
			Class<?> fieldtype = beanclass.getDeclaredField(fieldname).getType();
			Method method = beanclass.getDeclaredMethod(getSetterMethod(fieldname), new Class[] { fieldtype });
			method.invoke(beaninstance, new Object[] { typeCastObj(value,fieldtype) });
			return beaninstance;
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} 
		return null;
	}
	
	/**
	 * Typecasts an object
	 * @param fromObject
	 * @param fieldtype
	 * @return
	 */
	@SuppressWarnings("deprecation")
	public Object typeCastObj(Object fromObject, Class<?> fieldtype) {
		
		if (fromObject==null|| fromObject.toString().equals("")) {
			return null;
		}
		Object toObject=null;
			if (fromObject.getClass().isArray()) {
				if ( ((Object[])fromObject).length ==1) {
					fromObject = ((Object[])fromObject)[0];
				} else {
					log.error("The object contains multiple values. Unable to typecast it till my next version is released");
					return null;
				}
			}
		try {
			if (fieldtype.isAssignableFrom(fromObject.getClass())) {
				toObject= fieldtype.cast(fromObject);
			}
			else if (fieldtype.equals(Integer.class) || fieldtype.equals(int.class)) {
				toObject=Integer.parseInt(fromObject.toString());
			}
			else if (fieldtype.equals(String.class)) {
				toObject= fromObject.toString();
			}
			else if (fieldtype.equals(Boolean.class) || fieldtype.equals(boolean.class)) {
				toObject= Boolean.parseBoolean(fromObject.toString());
			}
			else if (fieldtype.equals(Date.class)) {
				toObject= Date.parse(fromObject.toString());
			}
		}
		catch (ClassCastException e) {
			log.error("Unable to cast object of type "+fromObject.getClass()+" to "+fieldtype.getClass());
			e.printStackTrace();
		}
		catch (IllegalArgumentException e) {
			e.printStackTrace();
		}
		return toObject;
	}
	
	public FieldInfo[] getBeanFieldsInfo()
	{
		return (this.bean != null) ?getBeanFieldsInfo(bean):null;
	}
	
	public FieldInfo[] getBeanFieldsInfo(Class<?> bean)
	{
		if (this.bean!=bean || this.fieldsInfo==null) {
			List<String> exclusion = Utils.getExclusions();
			FieldInfo []fieldsInfo=null;
			fieldsInfo = new FieldInfo[bean.getDeclaredFields().length];
			Integer i=0;
			for (Field field:bean.getDeclaredFields()) {
				if (! exclusion.contains(field.getName())) {
					fieldsInfo[i++] = new FieldInfo(field);
				}
			}
			this.fieldsInfo= new FieldInfo[i];
			for (FieldInfo fieldInfo:fieldsInfo) {
				if (fieldInfo !=null) {
					this.fieldsInfo[this.fieldsInfo.length - i--]=fieldInfo;
				}
			}
			this.bean=bean;
		}
		return this.fieldsInfo;
	}

	/**
	 * Overloaded: Returns all the fields of a bean set by setBean method.
	 * @return
	 */
	public String[] getBeanFields()
	{
		return (this.bean != null) ?getBeanFieldNames(bean):null;
	}
	
	/**
	 * Overloaded: Returns all the fieldnames of a bean
	 * @param bean
	 * @return
	 */
	public String[] getBeanFieldNames(Class<?> bean)
	{
		if (this.bean!=bean || this.fields==null) {
			List<String> exclusion = Utils.getExclusions();
			String []fields = new String[bean.getDeclaredFields().length];
			Integer i=0;
			for (Field field:bean.getDeclaredFields()) {
				if (! exclusion.contains(field.getName())) {
					fields[i++] = field.getName();
				}
			}
			this.fields= new String[i];
			for (String field:fields) {
				if (field !=null) {
					this.fields[this.fields.length - i--]=field;
				}
			}
			this.bean=bean;
		}
		return fields;
	}
		
	/**
	 * Returns the value of given fieldname of given object
	 * @param beaninstance
	 * @param fieldname
	 * @return
	 */
	public Object getFieldValue(Object beaninstance, String fieldname)
	{
		try {
			Class<?> bean = beaninstance.getClass();
			Method method = bean.getDeclaredMethod(	getGetterMethod(fieldname));
			return method.invoke(beaninstance);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * Returns default setter method for a given field
	 * @param fieldname
	 * @return
	 */
	public String getSetterMethod(String fieldname) {
		return "set"+toFirstUpper(fieldname);
		
	}
	
	/**
	 * Returns a default getter method of a given field
	 * @param fieldname
	 * @return
	 */
	private String getGetterMethod(String fieldname) {
		return "get"+toFirstUpper(fieldname);
		
	}

	/**
	 * Converts first character of a field to uppercase
	 * @param fieldname
	 * @return
	 */
	public String toFirstUpper(String fieldname) {
		return fieldname.replaceFirst(""+fieldname.charAt(0),
				""+Character.toUpperCase(fieldname.charAt(0)));
	}
	
	public ArrayList<Class<?>> getAllClasses(String PackageName) {
		Package pkg =Package.getPackage(PackageName);
		File directory = null;
		String relPath = pkg.getName().replace('.', '/');
		//URL resource = ClassLoader.getSystemClassLoader().getResource(relPath);
		URL resource = Thread.currentThread().getContextClassLoader().getResource(relPath);
		if (resource == null) {
			throw new RuntimeException("No resource for " + relPath);
		}
		try {
			directory = new File(resource.toURI());
		} catch (URISyntaxException e) {
			throw new RuntimeException(pkg.getName() + " (" + resource + ") does not appear to be a valid URL / URI", e);
		} catch (IllegalArgumentException e) {
			directory = null;
		}
		return (directory != null && directory.exists()) ? addClassesFromDir(pkg, directory): addClassesFromJar(pkg, directory, resource.getFile(), relPath);
	}

	private ArrayList<Class<?>> addClassesFromJar(Package pkg, File directory, String fullPath, String relPath) {
		ArrayList<Class<?>> classes = new  ArrayList<Class<?>>();
		try {
			String jarPath = fullPath.replaceFirst("[.]jar[!].*", ".jar").replaceFirst("file:", "");
			JarFile jarFile = new JarFile(jarPath);         
			Enumeration<JarEntry> entries = jarFile.entries();
			while(entries.hasMoreElements()) {
				JarEntry entry = entries.nextElement();
				String entryName = entry.getName();
				if(entryName.startsWith(relPath) && entryName.length() > (relPath.length() + "/".length())) {
					String className = entryName.replace('/', '.').replace('\\', '.').replace(".class", "");
					try {
						classes.add(Class.forName(className));
					} 
					catch (ClassNotFoundException e) {
						throw new RuntimeException("ClassNotFoundException loading " + className);
					}
				}
			}
		} catch (IOException e) {
			throw new RuntimeException(pkg.getName() + " (" + directory + ") does not appear to be a valid package", e);
		}
		return classes;
	}

	private ArrayList<Class<?>>  addClassesFromDir(Package pkg, File directory) {
		ArrayList<Class<?>> classes = new  ArrayList<Class<?>>();
		String[] files = directory.list();
		for (int i = 0; i < files.length; i++) {
		    if (files[i].endsWith(".class")) {
		        String className = pkg.getName() + '.' + files[i].substring(0, files[i].length() - 6);
		        try {
		            classes.add(Class.forName(className));
		        } 
		        catch (ClassNotFoundException e) {
		            throw new RuntimeException("ClassNotFoundException loading " + className);
		        }
		    }
		}
		return classes;
	}
}
